{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 贝叶斯决策论和朴素贝叶斯分类器\n",
    "\n",
    "徐迪深 浙江工商大学 金融学院 CFA1901"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 贝叶斯决策论\n",
    "\n",
    "​周末和朋友在一个大型商业综合体逛街吃饭，选择太多了，去吃什么好呢？好朋友聚在一起不容易，因此吃饭的选择会比较重要。如何选择呢？大家会提出各自建议：选一个口味符合的，选一个装修漂亮的，选一个生意兴旺的，选一个在美团上有很多介绍的，选一个有朋友去过的，诸如此类的建议你肯定需要尽可能的采纳，而且希望得到一个完美的交集。但是显然把所有的建议都遍历一遍的话，估计大家都不等不到吃饭了，而且你能找到收集全所有的建议吗？显然不能。但是选择还是要做的，饭总归要吃的。因此我们的决策是**基于一个不完备的信息集的**。此外，我们的决策过程是将有效的建议，一个一个根据重要性来尝试的，比如，我们认为一个好的餐馆，第一个也是最重要的特征是在网络平台上有好口碑，第二个次重要的特征是当天的生意好、客人多，第三重要的是我们当中有人去过的...... **我们的决策是一个不断的，但同时是有序的数据采集和信息分析的过程**。\n",
    "\n",
    "我们知道生活中绝大多数决策面临的信息都是不全的，我们只有有限的信息。在无法得到全面的信息的情况下，我们就在信息有限的情况下，尽可能做出一个好的预测。这个每天都在进行的过程就是大名鼎鼎的贝叶斯理论(Bayes theorem)，或者是贝叶斯推断(Bayes inference)，贝叶斯法则(Bayes rule).\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在可以设想一个场景。\n",
    "\n",
    "假如你想要去学校附近的宝龙广场的一个餐馆a吃饭。你去查看了网络上对这家餐馆的介绍，比如口味，装修，去的人多不多，想根据这些信息判断这个餐馆好不好。那么在贝叶斯决策论中，我们将诸如**口味，装修，客流量**这些信息称为特征，而餐馆**好**和**不好**称为类别。\n",
    "\n",
    "一个餐馆的口味，装修，去的人多不多都是餐馆的公开信息。那么基于这些信息，餐馆a是好餐馆（不好的餐馆也一样）的概率可以用条件概率表示：\n",
    "\n",
    "$$\n",
    "P(好餐馆|口味，装修，客流量)\\qquad(1)\n",
    "$$\n",
    "\n",
    "$(1)$式可以用贝叶斯公式展开，有\n",
    "\n",
    "$$\n",
    "P(好餐馆|口味，装修，客流量)=P(好餐馆)\\frac{P(口味，装修，客流量|好餐馆)}{P(口味，装修，客流量)}\\qquad(2)\n",
    "$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "显然我们希望$(1)$能越大越好。\n",
    "\n",
    "而将$(1)$得到$(2)$之后，可以发现$𝑃(好餐馆)$（我们也称为先验（prior）概率）是一个固定的值，同理，$P(口味，装修，客流量)$也是常数。在实际计算中，我们可以根据大数定理，用频率来估计概率。\n",
    "\n",
    "所以当$𝑃(口味，装修，客人的多少|好餐馆)$最大时，$(1)$也会最大。$𝑃(口味，装修，客流量|好餐馆)$这个我们叫做似然（likelihood）概率。而最大化似然概率我们最常用的就是极大似然估计。\n",
    "\n",
    "$(2)$也给了我们一个启示：\n",
    "\n",
    "$$\n",
    "\\frac{P(口味，装修，客流量|好餐馆)}{P(口味，装修，客流量)}\\qquad(3)\n",
    "$$\n",
    "\n",
    "假如$(3)$能够大于1，就意味着比起我们的总体样本，那就意味着这一类餐馆更有可能是好餐馆。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 极大似然估计\n",
    "\n",
    "回到上面一节我们的思路：想要$P(好餐馆|口味，装修，客流量)$更大，那么$P(口味，装修，客流量|好餐馆)$就要更大\n",
    "\n",
    "现在的问题是如何最大化似然概率。我们常用的方法就是极大似然估计。\n",
    "\n",
    "以极大似然估计$P(口味，装修，客流量|好餐馆)$为例。\n",
    "\n",
    "现在你在网上看到了N个人对于餐馆的评论，我们对每个人标记为$x_i$($i \\in N$),其中$x_i$={$口味=偏咸/偏甜，装修=豪华/简单，客流量=多/少$}，以及每个人的评价$y_i$={$好/不好$}（$i \\in N$）\n",
    "\n",
    "那么，就可以由这些人的评论得到如下一组概率\n",
    "\n",
    "$$\n",
    "P([口味=偏甜/偏咸，装修=豪华/简单，客流量=多/少]的各种情况的组合|y=好餐馆)\n",
    "$$\n",
    "\n",
    "实际上最后就是想找到一个最符合好餐厅的特征（例如我喜欢口味偏咸，装修简单，客流量少的餐厅，那么对于我来说$P(口味=咸，装修=简单，客流量=少|好参观)$这个概率就是最大的似然概率）"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 朴素贝叶斯分类器\n",
    "\n",
    "贝叶斯分类器有很多种，而朴素贝叶斯分类器是在所有特征都独立的假设进行分类的一种算法。至于为什么要做独立的假设（往往这个假设并不符合实际），是因为独立的假设使得计算简单很多，而对于预测的准确程度又不会损失到不能接受的程度。\n",
    "\n",
    "那么，上面的$(2)$式就可以写成\n",
    "\n",
    "$$\n",
    "P(好餐馆|口味，装修，客流量)=P(好餐馆)\\frac{P(口味|好餐馆)P(装修|好餐馆)P(客流量|好餐馆)}{P(口味)P(装修)P(客流量)}\\qquad(5)\n",
    "$$\n",
    "\n",
    "这样比起原来的式子，明显计算方面简单许多。但是，贝叶斯分类器也并不是只有朴素贝叶斯分类器一种，还有一类半朴素贝叶斯分类器，相对朴素的假设则放宽了许多，精度提高的同时计算也更加复杂。分类器的选用具体要根据任务的要求和背景决定"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 拉普拉斯修正\n",
    "\n",
    "有时候我们会遇到这样的情况。\n",
    "\n",
    "还是以判断餐馆a的好坏为例：你现在拿到了网上各个网友对餐馆a的口味，装修，客流量和对餐馆a的评价（好或者不好），但是你还多关注了一个特征：是否有音乐。但网上评价的人并不在意这个，所以没有人提起这个餐馆有没有音乐。于是有一个特征的特征值都缺失了。那么你在具体计算先验概率和似然概率甚至在计算贝叶斯公式中的分母（$P(音乐)$）的时候，这个概率为0（但分母肯定不可以为0）。\n",
    "\n",
    "你可以选择丢弃这个特征。但是你仍然很在意这个餐厅是否有音乐，所以并不想丢弃，那么该如何解决这个问题？\n",
    "\n",
    "拉普拉斯修正就是一个解决方法。\n",
    "\n",
    "拉普拉斯修正核心要保证的点是两个：第一个是让计算不违背数学的基本规律，第二个是不能让修正后的概率和修正之前有不可接受的差异（比如修正前我们的$P(音乐)$为0但修正后突然增加到0.8，这样会使得结果产生重大误差。）\n",
    "\n",
    "具体做法如下：\n",
    "\n",
    "还是以我们上面要判断餐馆a的好坏为例。其实在你的训练样本足够大的情况下，我们原来的对音乐取餐厅好坏的条件的似然概率计算是这样的：\n",
    "\n",
    "$$\n",
    "P(音乐=有|餐厅=好)=\\frac{有音乐且为好餐厅的个数=0}{好餐厅的个数}=0\n",
    "$$\n",
    "\n",
    "那么拉普拉斯修正就可以这样修正：\n",
    "\n",
    "$$\n",
    "P'(音乐=有|餐厅=好)=\\frac{有音乐且为好餐厅的个数+1}{好餐厅的个数+音乐这个特征的取值（有/无，所以是2）}\\neq 0\n",
    "$$\n",
    "\n",
    "然而拉普拉斯修正还不能改变信息集本身的特点。举个例子，假如你没有做拉普拉斯修正之前，你调查到的信息表示宝龙广场的特点是口味偏咸，装修简单，客流量大和没有音乐的餐馆更有可能是好餐馆，然而修正之后信息反应的是口味偏甜，装修豪华的，客流量小且有音乐的餐馆更有可能是好餐馆，那么显然这个修正是一个很失败的修正。\n",
    "\n",
    "所以既然上面音乐对餐厅好坏取条件的似然概率做了修正，那么其它特征都要相应做修正。\n",
    "\n",
    "先验概率的修正：\n",
    "$$\n",
    "P'(餐厅=好)=\\frac{好餐厅的个数+1}{所有餐厅的个数+所有特征的个数（口味，装修，客流量，音乐所以总共4个）}\n",
    "$$\n",
    "\n",
    "似然概率的修正，以口味的似然概率为例：\n",
    "\n",
    "$$\n",
    "P'(口味=偏咸|餐厅=好)=\\frac{口味偏咸且是好餐厅的个数+1}{好餐厅的个数+口味这个特征可能取值的个数（偏咸/偏甜，所以是2）}\n",
    "$$\n",
    "\n",
    "这样就能保证不舍弃音乐这个特征，且不改变原来训练样本（经验）的信息情况下，对目标进行分类（预测）"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 对于上面的问题另一种解决方法"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "拉普拉斯修正实际上就是保证不会对原样本产生大的改变的情况下，把频率都加了一个很小的数\n",
    "\n",
    "然而还有一种更加简单粗暴的方式：假如没有音乐的信息，那么直接在原来的基础上$\\times{1}$跳过这个特征就可以了，如果你并不认为这个特征足够重要的话，那么为了简化计算提高程序效率是可以这么做的，下面的案例也是用这个方法直接跳过了没被学习过的特征"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在网络上有很多相关的视频，可以帮助理解贝叶斯地的意义\n",
    "\n",
    "https://www.bilibili.com/video/BV1wf4y1d7iU?from=search&seid=16834708435849129354"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 朴素贝叶斯分类器的代码实现"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "对于朴素贝叶斯分类器，我们现在给出伪代码"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "    输入：样本集X={$x_1,x_2,x_3,...$},类别集Y={$y_1,y_2,y_3,...$}\n",
    "\n",
    "    过程：\n",
    "    1:计算每一个类别的先验概率P（y）（y属于Y）\n",
    "    2:计算每个特征和每一个类别对应的似然概率P（x|y）（x属于X，y属于Y）\n",
    "    3:根据类别将先验概率P（y）和似然概率P(x|y)连乘得到判别值\n",
    "\n",
    "    输出：\n",
    "    最大的判别值所对应的那个类别"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 贝叶斯分类器在金融的应用：信息分类"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "贝叶斯本身有许多实际应用: 垃圾邮件分类，搜索引擎中的推荐顺序等等。\n",
    "\n",
    "在金融实践中，贝叶斯分类器算法在处理一些非数字类信息和离散信息的优势被广泛应用，比如贷款者的信用评估，投资者风险偏好的评估，以及下面提到的信息的筛选"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>内容</th>\n",
       "      <th>标签</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>【摩根大通：MSCI本次指数调整料为中国带来71亿美元被动流入】摩根大通报告称，在MSCI将...</td>\n",
       "      <td>关注</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>【统计局：2018年城镇单位就业人员平均工资较快增长】国家统计局今天发布了2018年平均工资...</td>\n",
       "      <td>关注</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>【商务部：中国提交关于世贸组织改革的建议文件】商务部消息，5月13日，中国向世界贸易组织正...</td>\n",
       "      <td>关注</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>高通和HMD Global签订5G多模全球专利许可协议，将加快HMD Global的诺基亚品...</td>\n",
       "      <td>关注</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>大智慧快速拉升封涨停，封单近60万手。</td>\n",
       "      <td>关注</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                                  内容  标签\n",
       "0  【摩根大通：MSCI本次指数调整料为中国带来71亿美元被动流入】摩根大通报告称，在MSCI将...  关注\n",
       "1  【统计局：2018年城镇单位就业人员平均工资较快增长】国家统计局今天发布了2018年平均工资...  关注\n",
       "2   【商务部：中国提交关于世贸组织改革的建议文件】商务部消息，5月13日，中国向世界贸易组织正...  关注\n",
       "3  高通和HMD Global签订5G多模全球专利许可协议，将加快HMD Global的诺基亚品...  关注\n",
       "4                                大智慧快速拉升封涨停，封单近60万手。  关注"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "pd.read_excel('data_sina_fin_news2.xlsx').head()\n",
    "#展示数据"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "上面给出的是从新浪网上爬下来的的新闻，同时旁边附有标签。下面是对于该训练的样本的朴素贝叶斯分类器代码"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Building prefix dict from the default dictionary ...\n",
      "Loading model from cache C:\\Users\\EDWARD~1\\AppData\\Local\\Temp\\jieba.cache\n",
      "Loading model cost 0.639 seconds.\n",
      "Prefix dict has been built successfully.\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "正确率: 重点信息被关注: 0.856, 无关信息被忽略: 0.692, 错误率： 重点信息被忽略: 0.144, 无关信息被关注: 0.308.\n"
     ]
    }
   ],
   "source": [
    "import pandas as pd\n",
    "import jieba\n",
    "import re\n",
    "\n",
    "class news(object):\n",
    "    def __init__(self):\n",
    "        self.stats = {'focus': 0, 'ignore': 0, 'total': 0}#focus代表需要关注的信息数，ignore代表可以忽略的信息数，total是总和\n",
    "        self.keyword_freq = {}  \n",
    "        self.r = '[在从的得地多个百千万亿A-Za-z0-9_.!+-=——,$%^，。？、~@#￥%……&*：《》<>「」{}【】()/]'#这里我们需要去掉一些我们不需要的字符\n",
    "\n",
    "    def split(self,line):#将一串中文字符串分割成词，并放入列表当中\n",
    "        return jieba.lcut(line)\n",
    "    \n",
    "    def ProcessLine(self,massage):#这里的处理是将字符串中非中文以及一些常用的没有实际意义的助词介词去掉\n",
    "        return re.sub(self.r,'',massage)\n",
    "    \n",
    "    def add_keyword_freq(self, news, tag):        \n",
    "        for char in self.split(self.ProcessLine(news)):#遍历新闻中的所有关键词\n",
    "            if char in self.keyword_freq.keys():#如果关键词被记录了那么就在对应的标签计数\n",
    "                if tag == '关注':\n",
    "                    self.keyword_freq[str(char)]['focus'] += 1\n",
    "                else:\n",
    "                    self.keyword_freq[str(char)]['ignore'] += 1\n",
    "            else: # 如果该名字没有被记录，则增加键值\n",
    "                if tag == '关注':\n",
    "                    self.keyword_freq[str(char)]={'focus':1, 'ignore':0}\n",
    "                else:\n",
    "                    self.keyword_freq[str(char)]={'focus':0, 'ignore':1}\n",
    "        \n",
    "    def load_data(self, ndata): \n",
    "        for index, row in ndata.iterrows():\n",
    "            self.add_keyword_freq(row[0],row[1])#对每条新闻进行记录\n",
    "            if row[1] == '关注':\n",
    "                self.stats['focus'] += 1#记录各个标签的数目\n",
    "            else:\n",
    "                self.stats['ignore'] += 1\n",
    "        self.stats['total'] = self.stats['focus'] + self.stats['ignore']\n",
    "                \n",
    "        \n",
    "    def estimate_news(self, line, tag = 'focus'):#这里我们目的是输入一个类型为字符串的新闻\n",
    "        # prob 为先验概率\n",
    "        prob = self.stats['focus']/self.stats['total'] if tag == 'focus' \\\n",
    "        else self.stats['ignore']/self.stats['total'] # 根据所有样本来确定先验概率\n",
    "        prob = 0.5  # 简单设定先验概率\n",
    "        \n",
    "        for char in self.split(self.ProcessLine(line)): # 遍历名字中的每一个字，认为每一个字是独立的，此处忽略了名字是一个有意义的词语的情况\n",
    "            if str(char) in self.keyword_freq: # 正常情况，该名字出现过，有过统计\n",
    "                prob *= self.keyword_freq.get(str(char))[str(tag)] / self.stats.get(str(tag))\n",
    "            else:\n",
    "                # 概率乘以1，表示没有贡献，忽略该关键词的影响\n",
    "                prob *= 1\n",
    "        return prob\n",
    "    \n",
    "    def check_news(self, news, display = 1):\n",
    "        # 由于未考虑归一项，即 prob_f + prob_m 可能不等于 1，因此需要都做一遍\n",
    "        prob_f = self.estimate_news(news, 'focus')\n",
    "        prob_i = self.estimate_news(news, 'ignore')\n",
    "\n",
    "        if display == 1:\n",
    "            if prob_f > prob_i:\n",
    "                print('%s should raise attention with confidence level of %s'.format(news, round(1. * prob_f / (prob_i + prob_f),2)))\n",
    "            else:\n",
    "                print('%s should ignored with confidence level of %s'.format(news, round(1. * prob_i / (prob_i+ prob_f),2)))\n",
    "        else:\n",
    "            # 通过比标签为关注的概率和标签为其他的概率的大小来判断是男人还是女人名字\n",
    "            if prob_i > prob_f:\n",
    "                return 'ignore'\n",
    "            elif prob_i <= prob_f:\n",
    "                return 'focus'\n",
    "    \n",
    "    def data_split(self, data, f, i):\n",
    "        # 将数据分为两个部分，训练和测试。f是对标签为关注的测试量，i是对标签为其他的测试量\n",
    "        test = pd.concat([data[data['标签']=='关注'][:f],data[data['标签']=='其他'][:i]],ignore_index=True)\n",
    "        train = pd.concat([data[data['标签']=='关注'][f:],data[data['标签']=='其他'][i:]],ignore_index=True)\n",
    "        return train, test\n",
    "    \n",
    "    def testing(self,test, f, i):\n",
    "        self.test_res = {'ii':0, 'ff':0, 'if':0, 'fi':0, 'focus': f, 'ignore': i}\n",
    "        for index, row in test.iterrows():\n",
    "            res = self.check_news(row[0], display = 0)\n",
    "            if res == 'focus' and row[1] == '关注':\n",
    "                self.test_res['ff'] += 1\n",
    "            elif res == 'focus' and row[1] == '其他':\n",
    "                self.test_res['fi'] += 1\n",
    "            elif res == 'ignore' and row[1] == '其他':\n",
    "                self.test_res['ii'] += 1\n",
    "            elif res == 'ignore' and row[1] == '关注':\n",
    "                self.test_res['if'] += 1\n",
    "        print('正确率: 重点信息被关注: %s, 无关信息被忽略: %s, 错误率： 重点信息被忽略: %s, 无关信息被关注: %s.' %\\\n",
    "              (self.test_res['ff']/self.test_res['focus'], self.test_res['ii']/self.test_res['ignore'],self.test_res['if']/self.test_res['ignore'],self.test_res['fi']/self.test_res['focus']))\n",
    "\n",
    "  \n",
    "if __name__ == '__main__':#这个是保证上面的类被作为脚本直接执行而非用import方法\n",
    "    check = news()#将类实例化，可以理解为这个我们有一个工具包，必须给一个具体的人才能打开使用\n",
    "    data = pd.read_excel('data_sina_fin_news2.xlsx')#导入我们的数据\n",
    "    i = 500#确定我们要拿出来做测试集的样本容量\n",
    "    f = 500\n",
    "    train, test = check.data_split(data, f, i)\n",
    "    #下面是进行训练\n",
    "    check.load_data(train)\n",
    "    #下面是测试我们分类器的准确度\n",
    "    check.testing(test, f, i)\n",
    "    #下面被注释的代码是可以通过输入一条你未知的信息来判断的\n",
    "    #check.check_news(input('请在冒号后输入一条你想测试的新闻：'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "朴素贝叶斯分类器能达到70%的正确率就已经非常优秀了，很显然结果明显超过了70%，说明我们的分类器是很有效的"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
